﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using QuailDashboard.View.Data;
using QuailDashboard.View.Helpers;

namespace QuailDashboard.View.Controls
{
    public partial class VirtualizingTreeView : UserControl
    {
        private ObservableCollection<VirtualizingTreeViewItem> _itemsSource;

        #region Dependency Properties

        public static readonly DependencyProperty ItemTemplateProperty =
            DependencyProperty.Register("ItemTemplate", typeof(DataTemplate),
                                        typeof(VirtualizingTreeView),
                                        new PropertyMetadata(new PropertyChangedCallback(OnItemTemplatePropertyChanged)));

        public DataTemplate ItemTemplate
        {
            get { return (DataTemplate)GetValue(ItemTemplateProperty); }
            set { SetValue(ItemTemplateProperty, value); }
        }

        private static void OnItemTemplatePropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            ((VirtualizingTreeView)sender).LayoutRoot.ItemTemplate = (DataTemplate)e.NewValue;
        }

        public static readonly DependencyProperty ItemsSourceProperty =
            DependencyProperty.Register("ItemsSource", typeof(IEnumerable),
                                        typeof(VirtualizingTreeView),
                                        new PropertyMetadata(new PropertyChangedCallback(OnItemsSourcePropertyChanged)));

        public IEnumerable ItemsSource
        {
            get { return (IEnumerable)GetValue(ItemsSourceProperty); }
            set { SetValue(ItemsSourceProperty, value); }
        }
        
        /// <summary>
        /// When the ItemsSource property is set, create an underlying collection of VirtualizingTreeViewItems to hande display of tree nodes.
        /// Additionally, if the object passed in implements INotifyCollectionChanged, handle the CollectionChanged event of that collection appropriately.
        /// </summary>
        /// <param name="sender">Instance of VirtualizingTreeView</param>
        /// <param name="e">Event args</param>
        private static void OnItemsSourcePropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            var treeView = (VirtualizingTreeView)sender;

            if (e.NewValue is INotifyCollectionChanged)
            {
                ((INotifyCollectionChanged)e.NewValue).CollectionChanged += treeView.VirtualizingTreeView_CollectionChanged;
            }

            var observableItems = new ObservableCollection<VirtualizingTreeViewItem>();
            if (e.NewValue is IEnumerable)
            {
                var items = ((IEnumerable)e.NewValue).Cast<IHierarchalData>();

                observableItems =
                    new ObservableCollection<VirtualizingTreeViewItem>(
                        items.Select(item => new VirtualizingTreeViewItem(item, treeView, 0)));
            }

            treeView._itemsSource = observableItems;
            treeView.LayoutRoot.ItemsSource = observableItems;
        }

        /// <summary>
        /// If the tree is associated with an ObservableCollection, update the tree appropriately when that collection changes.
        /// </summary>
        /// <param name="sender">Not used</param>
        /// <param name="e">Collection changed event args</param>
        private void VirtualizingTreeView_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    for (int i = 0; i < e.NewItems.Count; i++)
                        _itemsSource.Insert(e.NewStartingIndex + i, new VirtualizingTreeViewItem((IHierarchalData)e.NewItems[i], this, 0));
                    break;
                case NotifyCollectionChangedAction.Reset:
                    _itemsSource.Clear();
                    break;
                case NotifyCollectionChangedAction.Remove:
                    var removedItems = e.OldItems.Cast<object>();
                    var treeItemsToRemove = _itemsSource.Where(item => removedItems.Any(x => ReferenceEquals(x, item.Data) || ReferenceEquals(x, item.Data.Parent))).ToList();
                    foreach (var toRemove in treeItemsToRemove)
                    {
                        _itemsSource.Remove(toRemove);
                    }
                    break;
                default:
                    // Replace
                    throw new NotImplementedException(string.Format("VirtualizingTreeView does not implement the \"{0}\" action for underlying ObservableCollections.", e.Action));
                    break;
            }
        }

        #endregion

        #region Events

        public event RoutedPropertyChangedEventHandler<Object> SelectedItemChanged;
        private void RaiseSelectedItemChanged(object oldValue, object newValue)
        {
            if (SelectedItemChanged != null)
            {
                SelectedItemChanged(this, new RoutedPropertyChangedEventArgs<object>(oldValue, newValue));
            }
        }

        public delegate void OnFindTreeVewItem(object sender, VirtualizingTreeViewItem tvitem, IHierarchalData rawdata);
        public event OnFindTreeVewItem FindTreeViewItemComplete;

        #endregion

        #region Properties

        /// <summary>
        /// Returns the IHierarchalData associated with the selected item (null if nothing is selected)
        /// </summary>
        public object SelectedItem
        {
            get
            {
                var selectedItem = LayoutRoot.SelectedItem as VirtualizingTreeViewItem;
                
                if (selectedItem == null)
                    return null;

                return selectedItem.Data;
            }
        }

        /// <summary>
        /// Returns the IHierarchalData objects associated with root-level nodes
        /// </summary>
        public IEnumerable<IHierarchalData> Items
        {
            get { return _itemsSource.Where(item => item.Depth == 0).Select(item => item.Data); }
        }

        #endregion

        public VirtualizingTreeView()
        {
            InitializeComponent();

            LayoutRoot.LayoutUpdated += Control_LayoutUpdated;
            LayoutRoot.SelectionChanged += LayoutRoot_SelectionChanged;
        }

        /// <summary>
        /// When the ListBox's SelectionChanged event fires, raise the SelectionChanged event for the tree.
        /// </summary>
        /// <param name="sender">Not used</param>
        /// <param name="e">Event arguments for the ListBox's SelectionChanged event</param>
        private void LayoutRoot_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            // RemovedItems and AddedItems should have no more than 1 items each
            var oldValue = e.RemovedItems.Count == 1 ? e.RemovedItems[0] : null;
            var newValue = e.AddedItems.Count == 1 ? e.AddedItems[0] : null;

            RaiseSelectedItemChanged(oldValue, newValue);
        }

        #region Find Item

        private IHierarchalData _pendingSelect;
        private bool _selectFoundItem;

        /// <summary>
        /// Looks for a treeview item, firing FindTreeVewItemComplete when the item is found or the find is complete
        /// </summary>
        /// <param name="data">the data to look for</param>
        public void FindTreeViewItem(IHierarchalData data)
        {
            FindTreeViewItem(data, true);
        }

        /// <summary>
        /// Looks for a treeview item, firing FindTreeVewItemComplete when the item is found or the find is complete
        /// </summary>
        /// <param name="data">the data to look for</param>
        /// <param name="selectwhenfound">set to true to select the found item</param>
        public void FindTreeViewItem(IHierarchalData data, bool selectwhenfound)
        {
            _pendingSelect = data;
            _selectFoundItem = selectwhenfound;
            ContinueFind();
        }

        /// <summary>
        /// Gets a HierarchalTreeViewItem based on a IHierarchalData object - may be async
        /// </summary>
        /// <param name="data">The data to find</param>
        /// <param name="didExpand">if this value is true, </param>
        /// <returns>the treeview item it found</returns>
        private VirtualizingTreeViewItem GetTreeViewItem(IHierarchalData data, ref bool didExpand)
        {
            if (data == null)
                return null;

            var tvitem = _itemsSource.FirstOrDefault(item => ReferenceEquals(item.Data, data));

            if (tvitem != null)
                return tvitem;

            tvitem = GetTreeViewItem(data.Parent, ref didExpand);

            if (tvitem == null)
                return null;

            didExpand = true;
            tvitem.IsExpanded = true;
            LayoutRoot.ScrollIntoView(tvitem);

            return _itemsSource.FirstOrDefault(item => ReferenceEquals(item.Data, data));
        }

        private void Control_LayoutUpdated(object sender, EventArgs e)
        {
            if (_pendingSelect != null)
                ContinueFind();
        }

        private void ContinueFind()
        {
            if (ItemsSource == null)
                return;

            var didExpand = false;
            var tvitem = GetTreeViewItem(_pendingSelect, ref didExpand);

            if (tvitem != null || !didExpand)
            {
                if (_selectFoundItem && tvitem != null)
                    LayoutRoot.SelectedItem = tvitem;

                var select = _pendingSelect;
                _pendingSelect = null;

                if (FindTreeViewItemComplete != null)
                    FindTreeViewItemComplete(this, tvitem, select);
            }
        }

        #endregion

        #region Expand/Collapse

        /// <summary>
        /// When a container is loaded in the tree, add an event handler for double-click events.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void TreeItemContainer_Loaded(object sender, RoutedEventArgs e)
        {
            var container = (FrameworkElement) sender;
            var treeViewItem = (VirtualizingTreeViewItem) container.DataContext;

            container.AddDoubleClick((senderMouse, eMouse) =>
                                         {
                                             treeViewItem.IsExpanded = !treeViewItem.IsExpanded;
                                         });
        }
    
        /// <summary>
        /// When the Expand/Collapse button is clicked, ensure that the node associated with that button is selected in the tree.
        /// </summary>
        /// <param name="sender">ExpanderButton that was clicked</param>
        /// <param name="e">Not used</param>
        private void ExpanderButton_Click(object sender, RoutedEventArgs e)
        {
            var treeViewItem = (VirtualizingTreeViewItem)((FrameworkElement)sender).DataContext;
            LayoutRoot.SelectedItem = treeViewItem;
        }

        /// <summary>
        /// Expand a node by adding its visible descendants to the ListBox's underlying collection.
        /// </summary>
        /// <param name="toExpand">Node to expand</param>
        /// <returns>Index at which last node was inserted</returns>
        protected int Expand(VirtualizingTreeViewItem toExpand)
        {
            var index = _itemsSource.IndexOf(toExpand);
            foreach (var child in toExpand.Children)
            {
                _itemsSource.Insert(++index, child);

                if (child.IsExpanded && child.Children != null)
                    index = Expand(child);
            }
            return index;
        }

        /// <summary>
        /// Collapse a node by removing its visible descendants from the ListBox's underlying collection.
        /// </summary>
        /// <param name="toCollapse">Node to collapse</param>
        protected void Collapse(VirtualizingTreeViewItem toCollapse)
        {
            var removeCount = GetVisibleDescendantCount(toCollapse);

            var startIndex = _itemsSource.IndexOf(toCollapse);

            for (var i = removeCount; i > 0; i--)
            {
                _itemsSource.RemoveAt(startIndex + i);
            }
        }

        /// <summary>
        /// Recursively count the number of descendant nodes that are visible
        /// </summary>
        /// <param name="toCollapse">Node that is expanded</param>
        /// <returns>Number of node's descendants that are visible</returns>
        private static int GetVisibleDescendantCount(VirtualizingTreeViewItem toCollapse)
        {
            var count = 0;

            foreach (var child in toCollapse.Children)
            {
                count++;

                if (child.IsExpanded && child.Children != null)
                    count += GetVisibleDescendantCount(child);
            }

            return count;
        }

        #endregion

        /// <summary>
        /// This class is used to wrap instances of IHierarchalData in order to use them in conjunction with the VirtualizingTreeView.
        /// This class keeps track of a node's depth, whether its Expander is visible, and whether it is expanded.
        /// </summary>
        public class VirtualizingTreeViewItem : INotifyPropertyChanged
        {
            private VirtualizingTreeView _treeView;

            #region Properties

            public int Depth { get; private set; }

            /// <summary>
            /// The left margin for the node. This creates the illusion of one item in the list being a descendant of another.
            /// </summary>
            public Thickness Indent
            {
                get { return new Thickness(15 * Depth, 0, 0, 0); }
            }

            /// <summary>
            /// The Expander button is only visible if a node has at least one child
            /// </summary>
            public Visibility ExpanderVisibility
            {
                get { return Children == null || Children.Count == 0 ? Visibility.Collapsed : Visibility.Visible; }
            }

            /// <summary>
            /// Raw data displayed within the node.
            /// </summary>
            public IHierarchalData Data { get; set; }

            private List<VirtualizingTreeViewItem> _children;
            /// <summary>
            /// Wraps the Items collection of the Data object as VirtualizingTreeViewItems and returns this collection.
            /// </summary>
            public List<VirtualizingTreeViewItem> Children 
            { 
                get
                {
                    if (_children.Count == 0 && Data.IsPopulated)
                    {
                        var childTreeViewItems =
                            Data.Items.Cast<IHierarchalData>().Select(
                                item => new VirtualizingTreeViewItem(item, _treeView, Depth + 1));

                        _children.AddRange(childTreeViewItems);
                    }

                    return _children;
                }
            }

            private bool _isExpanded;
            /// <summary>
            /// Expands/Collapses the node. If the node is expanded, all child nodes will be populates.
            /// </summary>
            public bool IsExpanded
            {
                get { return _isExpanded; }
                set
                {
                    if (_children.Count > 0)
                    {
                        _isExpanded = value;

                        if (_isExpanded)
                        {
                            _treeView.Expand(this);

                            PopulateItems(Data.Items.Cast<IHierarchalData>().Where(item => !item.IsPopulated));
                        }
                        else
                        {
                            _treeView.Collapse(this);
                        }

                        RaisePropertyChanged("IsExpanded");
                    }
                }
            }

            public bool IsVisible
            {
                get { return _treeView._itemsSource.Contains(this); }
            }

            #endregion

            public event PropertyChangedEventHandler PropertyChanged;

            private void RaisePropertyChanged(string propertyName)
            {
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }

            public VirtualizingTreeViewItem(IHierarchalData data, VirtualizingTreeView treeView, int depth)
            {
                Data = data;
                _treeView = treeView;
                Depth = depth;
                _children = new List<VirtualizingTreeViewItem>();
                data.PopulateComplete += data_PopulateComplete;

                if (data.Items is INotifyCollectionChanged)
                {
                    ((INotifyCollectionChanged)data.Items).CollectionChanged += VirtualizingTreeViewItem_CollectionChanged;
                }
            }

            /// <summary>
            /// If the Items collection of the Data property is an ObservableCollection, update the tree appropriately when that collection changes.
            /// </summary>
            /// <param name="sender">Not used</param>
            /// <param name="e">Collection changed event args</param>
            private void VirtualizingTreeViewItem_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
            {
                switch (e.Action)
                {
                    case NotifyCollectionChangedAction.Add:
                        if (IsExpanded && IsVisible)
                        {
                            var newItems = new List<IHierarchalData>();
                            _treeView.Collapse(this);
                            var newStartingIndex = e.NewStartingIndex > _children.Count ? _children.Count : e.NewStartingIndex;
                            for (var i = 0; i < e.NewItems.Count; i++)
                            {
                                var newItem = (IHierarchalData) e.NewItems[i];
                                newItems.Add(newItem);
                                _children.Insert(newStartingIndex + i, new VirtualizingTreeViewItem(newItem, _treeView, Depth + 1));
                            }
                            _treeView.Expand(this);
                            // Make sure all the newly inserted items get populated
                            PopulateItems(newItems);
                        }
                        else
                        {
                            var newStartingIndex = e.NewStartingIndex > _children.Count ? _children.Count : e.NewStartingIndex;
                            for (var i = 0; i < e.NewItems.Count; i++)
                            {
                                _children.Insert(newStartingIndex + i, new VirtualizingTreeViewItem((IHierarchalData)e.NewItems[i], _treeView, Depth + 1));
                            }
                        }
                        break;
                    case NotifyCollectionChangedAction.Reset:
                        if (IsExpanded && IsVisible)
                        {
                            // If Items collection of an expanded node is reset, collapse the node and clear the _children collection
                            _treeView.Collapse(this);
                            _children.Clear();
                        }
                        else
                        {
                            // If Items collection of a collapsed node is reset, clear the _children collection
                            _children.Clear();
                        }
                        break;
                    case NotifyCollectionChangedAction.Remove:
                        var removedItems = e.OldItems.Cast<object>();
                        if (IsExpanded && IsVisible)
                        {
                            // If items are removed from the Items collection of an expanded node, collapse each item to remove (so descendants are removed)
                            // and remove from underlying collection
                            var treeItemsToRemove = _treeView._itemsSource.Where(item => removedItems.Any(x => ReferenceEquals(x, item.Data))).ToList();
                            foreach (var toRemove in treeItemsToRemove)
                            {
                                _treeView.Collapse(toRemove);
                                _treeView._itemsSource.Remove(toRemove);
                            }
                        }
                        else
                        {
                            // If items are removed from the Items collection of a collapsed node, remove each item from the _children collection (so they
                            // will not be shown when node is expanded)
                            var treeItemsToRemove = _children.Where(item => removedItems.Any(x => ReferenceEquals(x, item.Data))).ToList();        
                            foreach (var toRemove in treeItemsToRemove)
                            {
                                _children.Remove(toRemove);
                            }
                        }
                        break;
                    default:
                        // Replace
                        throw new NotImplementedException(string.Format("VirtualizingTreeView does not implement the \"{0}\" action for underlying ObservableCollections.", e.Action));
                        break;
                }
            }

            /// <summary>
            /// When a node is populated, the Expander will be shown if it has at least one child.
            /// </summary>
            /// <param name="sender">Not used</param>
            /// <param name="e">Not used</param>
            private void data_PopulateComplete(object sender, EventArgs e)
            {
                RaisePropertyChanged("ExpanderVisibility");
            }

            /// <summary>
            /// Calls Populate() on children in a way that is "friendly" to other nodes
            /// </summary>
            /// <remarks>
            /// If a given node has a lot of children, calling Populate() on all the children can force other calls to Populate() (e.g., from a 
            /// second node's children) to wait until all original calls have finished. This means that when another node is expanded, its children will not be 
            /// populated until the first node's children finish, which may take a long time. This method splits up the calls to Populate()
            /// into chunks of 30 every 3 seconds, allowing "simultaneous" population of nodes regardless of the number of their children.
            /// </remarks>
            /// <param name="items">Items to populate</param>
            private static void PopulateItems(IEnumerable<IHierarchalData> items)
            {
                var chunkSize = 30;

                // Do this in a separate thread so we don't block other nodes from expanding
                ThreadPool.QueueUserWorkItem(state =>
                {
                    while (items.Count() > 0)
                    {
                        var chunk = items.Take(chunkSize).ToList();
                        items = items.Skip(chunkSize).ToList();

                        // Make sure Populate() is invoked on the UI thread to avoid cross-thread access exceptions
                        Deployment.Current.Dispatcher.BeginInvoke(() => chunk.ForEach(item => item.Populate()));

                        // Give other threads a chance
                        Thread.Sleep(3000);
                    }
                });
            }
        }
    }
}
